/*
 * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.util.zip;

import java.io.SequenceInputStream;
import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.EOFException;

/**
 * This class implements a stream filter for reading compressed data in
 * the GZIP file format.
 *
 * @author David Connelly
 * @see InflaterInputStream
 */
public class GZIPInputStream extends InflaterInputStream {

  /**
   * CRC-32 for uncompressed data.
   */
  protected CRC32 crc = new CRC32();

  /**
   * Indicates end of input stream.
   */
  protected boolean eos;

  private boolean closed = false;

  /**
   * Check to make sure that this stream has not been closed
   */
  private void ensureOpen() throws IOException {
    if (closed) {
      throw new IOException("Stream closed");
    }
  }

  /**
   * Creates a new input stream with the specified buffer size.
   *
   * @param in the input stream
   * @param size the input buffer size
   * @throws ZipException if a GZIP format error has occurred or the compression method used is
   * unsupported
   * @throws IOException if an I/O error has occurred
   * @throws IllegalArgumentException if {@code size <= 0}
   */
  public GZIPInputStream(InputStream in, int size) throws IOException {
    super(in, new Inflater(true), size);
    usesDefaultInflater = true;
    readHeader(in);
  }

  /**
   * Creates a new input stream with a default buffer size.
   *
   * @param in the input stream
   * @throws ZipException if a GZIP format error has occurred or the compression method used is
   * unsupported
   * @throws IOException if an I/O error has occurred
   */
  public GZIPInputStream(InputStream in) throws IOException {
    this(in, 512);
  }

  /**
   * Reads uncompressed data into an array of bytes. If <code>len</code> is not
   * zero, the method will block until some input can be decompressed; otherwise,
   * no bytes are read and <code>0</code> is returned.
   *
   * @param buf the buffer into which the data is read
   * @param off the start offset in the destination array <code>b</code>
   * @param len the maximum number of bytes read
   * @return the actual number of bytes read, or -1 if the end of the compressed input stream is
   * reached
   * @throws NullPointerException If <code>buf</code> is <code>null</code>.
   * @throws IndexOutOfBoundsException If <code>off</code> is negative, <code>len</code> is
   * negative, or <code>len</code> is greater than <code>buf.length - off</code>
   * @throws ZipException if the compressed input data is corrupt.
   * @throws IOException if an I/O error has occurred.
   */
  public int read(byte[] buf, int off, int len) throws IOException {
    ensureOpen();
    if (eos) {
      return -1;
    }
    int n = super.read(buf, off, len);
    if (n == -1) {
      if (readTrailer()) {
        eos = true;
      } else {
        return this.read(buf, off, len);
      }
    } else {
      crc.update(buf, off, n);
    }
    return n;
  }

  /**
   * Closes this input stream and releases any system resources associated
   * with the stream.
   *
   * @throws IOException if an I/O error has occurred
   */
  public void close() throws IOException {
    if (!closed) {
      super.close();
      eos = true;
      closed = true;
    }
  }

  /**
   * GZIP header magic number.
   */
  public final static int GZIP_MAGIC = 0x8b1f;

  /*
   * File header flags.
   */
  private final static int FTEXT = 1;    // Extra text
  private final static int FHCRC = 2;    // Header CRC
  private final static int FEXTRA = 4;    // Extra field
  private final static int FNAME = 8;    // File name
  private final static int FCOMMENT = 16;   // File comment

  /*
   * Reads GZIP member header and returns the total byte number
   * of this member header.
   */
  private int readHeader(InputStream this_in) throws IOException {
    CheckedInputStream in = new CheckedInputStream(this_in, crc);
    crc.reset();
    // Check header magic
    if (readUShort(in) != GZIP_MAGIC) {
      throw new ZipException("Not in GZIP format");
    }
    // Check compression method
    if (readUByte(in) != 8) {
      throw new ZipException("Unsupported compression method");
    }
    // Read flags
    int flg = readUByte(in);
    // Skip MTIME, XFL, and OS fields
    skipBytes(in, 6);
    int n = 2 + 2 + 6;
    // Skip optional extra field
    if ((flg & FEXTRA) == FEXTRA) {
      int m = readUShort(in);
      skipBytes(in, m);
      n += m + 2;
    }
    // Skip optional file name
    if ((flg & FNAME) == FNAME) {
      do {
        n++;
      } while (readUByte(in) != 0);
    }
    // Skip optional file comment
    if ((flg & FCOMMENT) == FCOMMENT) {
      do {
        n++;
      } while (readUByte(in) != 0);
    }
    // Check optional header CRC
    if ((flg & FHCRC) == FHCRC) {
      int v = (int) crc.getValue() & 0xffff;
      if (readUShort(in) != v) {
        throw new ZipException("Corrupt GZIP header");
      }
      n += 2;
    }
    crc.reset();
    return n;
  }

  /*
   * Reads GZIP member trailer and returns true if the eos
   * reached, false if there are more (concatenated gzip
   * data set)
   */
  private boolean readTrailer() throws IOException {
    InputStream in = this.in;
    int n = inf.getRemaining();
    if (n > 0) {
      in = new SequenceInputStream(
          new ByteArrayInputStream(buf, len - n, n),
          new FilterInputStream(in) {
            public void close() throws IOException {
            }
          });
    }
    // Uses left-to-right evaluation order
    if ((readUInt(in) != crc.getValue()) ||
        // rfc1952; ISIZE is the input size modulo 2^32
        (readUInt(in) != (inf.getBytesWritten() & 0xffffffffL))) {
      throw new ZipException("Corrupt GZIP trailer");
    }

    // If there are more bytes available in "in" or
    // the leftover in the "inf" is > 26 bytes:
    // this.trailer(8) + next.header.min(10) + next.trailer(8)
    // try concatenated case
    if (this.in.available() > 0 || n > 26) {
      int m = 8;                  // this.trailer
      try {
        m += readHeader(in);    // next.header
      } catch (IOException ze) {
        return true;  // ignore any malformed, do nothing
      }
      inf.reset();
      if (n > m) {
        inf.setInput(buf, len - n + m, n - m);
      }
      return false;
    }
    return true;
  }

  /*
   * Reads unsigned integer in Intel byte order.
   */
  private long readUInt(InputStream in) throws IOException {
    long s = readUShort(in);
    return ((long) readUShort(in) << 16) | s;
  }

  /*
   * Reads unsigned short in Intel byte order.
   */
  private int readUShort(InputStream in) throws IOException {
    int b = readUByte(in);
    return (readUByte(in) << 8) | b;
  }

  /*
   * Reads unsigned byte.
   */
  private int readUByte(InputStream in) throws IOException {
    int b = in.read();
    if (b == -1) {
      throw new EOFException();
    }
    if (b < -1 || b > 255) {
      // Report on this.in, not argument in; see read{Header, Trailer}.
      throw new IOException(this.in.getClass().getName()
          + ".read() returned value out of range -1..255: " + b);
    }
    return b;
  }

  private byte[] tmpbuf = new byte[128];

  /*
   * Skips bytes of input data blocking until all bytes are skipped.
   * Does not assume that the input stream is capable of seeking.
   */
  private void skipBytes(InputStream in, int n) throws IOException {
    while (n > 0) {
      int len = in.read(tmpbuf, 0, n < tmpbuf.length ? n : tmpbuf.length);
      if (len == -1) {
        throw new EOFException();
      }
      n -= len;
    }
  }
}
